package org.adorsys.aderp.jpa.impl;

import java.io.Serializable;
import java.util.List;

import javax.persistence.EntityManager;

import org.adorsys.aderp.jpa.api.audit.AbstractAuditRecord;
import org.adorsys.aderp.jpa.api.audit.AbstractAuditRecordBuilder;
import org.adorsys.aderp.jpa.api.audit.ChangingImmutable;
import org.adorsys.aderp.jpa.api.audit.ModifMode;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

/**
 * Integrate auditing and enforces immutable conditions on business keys.
 * 
 * @author francis
 *
 * @param <T>
 * @param <ID>
 */
public class AderpJPARepository<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements ChangingImmutable<T> {
	
	private AuditRecordInfo<? extends AbstractAuditRecord> auditRecordInfo;
	private BusinessKeyInfo<T,ID> businessKeyInfo;
	private List<ToOneRelationshipInfo> toOneRelationshipInfos;
	private List<OneToManyRelationShipInfo> oneToManyRelationShipInfos;
	
//	private JpaEntityInformation<T, ?> entityInformation;
	
	public AderpJPARepository(JpaEntityInformation<T, ?> entityInformation,
			EntityManager entityManager, BeanFactory beanFactory) {
		super(entityInformation, entityManager);
//		this.entityInformation = entityInformation;
		Class<T> javaType = entityInformation.getJavaType();
		RepositoryMap repositoryMap = beanFactory.getBean(RepositoryMap.class);
		repositoryMap.put(javaType, this);
		// check installation of capability
		AbstractAuditRecordBuilder abstractAuditRecordBuilder = beanFactory.getBean(AbstractAuditRecordBuilder.class);
		if (abstractAuditRecordBuilder==null) return;
		
		businessKeyInfo = BusinessKeyInfoBuilder.buildBusinessKeyInfo(javaType);
		if(businessKeyInfo!=null){
			
			auditRecordInfo = AuditRecordInfoBuilder.buildAuditRecordInfo(
					javaType, beanFactory, null);
			
			toOneRelationshipInfos = ToOneRelationshipInfoBuilder.build(javaType, beanFactory);
			
			oneToManyRelationShipInfos = OneToManyRelationshipInfoBuilder.build(
					javaType, beanFactory);
		}
	}

	/**
	 * Proceed audit
	 */
	@Override
	public void delete(T entity) {
		createAuditRecord(entity, true);
		super.delete(entity);
	}

	/**
	 * Implementation will iterate through and call delete on each entity.
	 */
	@Override
	public void delete(Iterable<? extends T> entities) {
		super.delete(entities);
	}

	/**
	 * Proceed audit
	 */
	@Override
	public void deleteInBatch(Iterable<T> entities) {
		// proceed through audit for auditable.
		if(isAuditableentity()){
			List<T> findAll = findAll();
			delete(findAll);
		} else {
			super.deleteInBatch(entities);
		}
	}

	/**
	 * Proceed audit
	 */
	@Override
	public void deleteAll() {
		// proceed through audit for auditable.
		if(isAuditableentity()){
			List<T> findAll = findAll();
			deleteInBatch(findAll);
		} else {
			super.deleteAll();
		}
	}

	/**
	 * Proceed audit
	 */
	@Override
	public T save(T entity) {
		restoreImmutable(entity);// only for update		
		
		attachRelationships(entity);// independent on whether persist or update
		
		createAuditRecord(entity, false);

		return super.save(entity);
	}
	
	public T changeImmutable(String businessKey, String fieldName, Object value){
		if(businessKeyInfo==null) throw new IllegalStateException("Business key field not defined.");
		T orig = findByBusinessKey(businessKey);
		businessKeyInfo.setImmutable(orig, fieldName, value);
		createAuditRecord(orig, false);
		return super.save(orig);
	}
	
	/**
	 * @param entity
	 */
	private void attachRelationships(T entity){
		if(toOneRelationshipInfos!=null){
			for (ToOneRelationshipInfo toOneRelationshipInfo : toOneRelationshipInfos) {
				toOneRelationshipInfo.attachField(entity);
			}
		}
		
		if(oneToManyRelationShipInfos!=null){
			for (OneToManyRelationShipInfo oneToManyRelationShipInfo : oneToManyRelationShipInfos) {
				oneToManyRelationShipInfo.attachField(entity);
			}
		}
	}
	
	private void createAuditRecord(T entity, boolean deleting) {
		createAuditRecord(entity, deleting, null);
	}
	
	private void createAuditRecord(T entity, boolean deleting, String envData) {
		if(!isAuditableentity()) return;
		String businessKey = businessKeyInfo.getBusinessKey(entity);
		ModifMode mode = ModifMode.CREATED;
		if(businessKeyInfo.getId(entity)!=null) {
			if(deleting){
				mode = ModifMode.DELETED;
			} else {
				mode = ModifMode.CHANGED;
			}
		}
		auditRecordInfo.createAuditRecord(businessKey, entity, mode, envData);
	}
	
	/**
	 * an entity is auditable when following conditions are met.
	 * @return
	 */
	private boolean isAuditableentity(){
		return auditRecordInfo!=null;
	}
	
	private void restoreImmutable(T entity)
	{
		if(businessKeyInfo==null) return;
		
		String businessKey = businessKeyInfo.getBusinessKey(entity);
		T orig = findByBusinessKey(businessKey);
		
    	if(orig==null){
    		businessKeyInfo.setId(entity, null);
    		businessKeyInfo.setVersion(entity, null);
    	} else {
    		businessKeyInfo.setId(entity, businessKeyInfo.getId(orig));
    		businessKeyInfo.setVersion(entity, businessKeyInfo.getVersion(orig));
    		businessKeyInfo.restoreImmutableField(entity, orig);
    	}
	}
	
	public T findByBusinessKey(final String businessKey) {
		Specification<T> bkSpec = businessKeyInfo.makeSpecification(businessKeyInfo.getBusinessKeyFieldName(), businessKey);		
		return findOne(bkSpec);
	}

	public T findByBusinessKey(final T entity) {
		String businessKey = businessKeyInfo.getBusinessKey(entity);
		Specification<T> bkSpec = businessKeyInfo.makeSpecification(businessKeyInfo.getBusinessKeyFieldName(), businessKey);		
		return findOne(bkSpec);
	}
}
